This renewal is incremental, backwards compatible, and takes current trends from front-end frameworks into account. Developer experience and performance are the primary goals of this renewal movement. Standalone components and signals are two well-known features that have already emerged as part of this effort.
Angular 17 adds to the Angular Renaissance in fall 2023 with a new syntax for the control flow, delayed page loading, improved SSR support, and the CLI now relies on esbuild, significantly speeding up the build.
In this article, I will discuss these new features using an example application (Figure 1). The source code used can be found under Example.
Figure 1: Example application
New syntax for control flow in templates
Angular has used structural directives such as *ngIf or *ngFor for the control flow since its inception. Because the control flow needed to be extensively revised for Angular 16’s signals anyway, the Angular team decided to give it a complete overhaul. The result is a new built-in control flow that stands out clearly from the rendered markup (Listing 1).
Listing 1
@for (product of products(); track product.id) { <div class="card"> <h2 class="card-title">{{product.productName}}</h2> […] </div> } @empty { <p class="text-lg">No Products found!</p> }
It’s worth noting the new @empty block, which Angular renders if the list to be iterated is empty.
Even if signals were a driver for this new syntax, they’re not a prerequisite for its use. The new control flow blocks can also be used with classic variables or with observables in conjunction with the async pipe.
While signals were a motivation for this new syntax, they are not required for its use.
iJS Newsletter
Keep up with JavaScript’s latest news!
The mandatory track expression allows Angular to identify individual elements that have been moved within the iterated collection. This drastically reduces the rendering effort and allows existing DOM nodes to be reused. When iterating collections of primitive types, e.g. number or string, track should be used with the pseudo variable $index according to the Angular team (Listing 2).
Listing 2
@for (group of groups(); track $index) { <a (click)="groupSelected(group)">{{group}}</a> @if (!$last) { <span class="mr-5 ml-5">|</span> } }
In addition to $index, the other values known from *ngFor are also available via pseudo variables: $count, $first, $last, $even, $odd. If required, their values can be stored in template variables using expressions (Listing 3).
Listing 3
@for (group of groups(); track $index; let isLast = $last) { <a (click)="groupSelected(group)">{{group}}</a> @if (!isLast) { <span class="mr-5 ml-5">|</span> } }
The new @if simplifies the formulation of else/ else-if branches (Listing 4).
Listing 4
@if (product().discountedPrice && product().discountMinCount) { […] } @else if (product().discountedPrice && !product().discountMinCount) { […] } @else { […] }
In addition, different cases can also be distinguished with a @switch (Listing 5).
Listing 5
@switch (mode) { @case ('full') {
Originally, the Angular CLI used webpack to build bundles. However, webpack is a bit outdated and is currently being challenged by newer tools that are easier to use and much faster. One of these tools is esbuild, which has a notable adoption rate of over 20,000 downloads per week.
[… } @case ('small') { […] } @default { […] } }
In contrast to ngSwitch and *ngSwitchCase, the new syntax is type-safe. In this example, the individual @case blocks must have string values, especially as the variable mode passed to @switch is also of type string.
The new control flow syntax reduces the need to use structural directives, which are powerful but sometimes unnecessarily complex. Nevertheless, the framework will continue to support structural directives. On the one hand, there are some valid use cases for this and, on the other hand, the framework must be backwards compatible despite the many exciting new features.
Box: Automatic migration to Build-in Control Flow
If you want to migrate your program code automatically to the new control flow syntax, you can now find a schematic for this in the @angular/core package:
ng g @angular/core:control-flow
Delayed loading of side panels
Typically, not all areas of a page are equally important. Product suggestions are typically secondary to the product itself on a product detail page. However, this changes when the user scrolls the product suggestions into view in the browser window, or viewport.
For performance-critical web applications like online stores, it’s advisable to delay loading less important page sections. This ensures that the most important elements are available more quickly. Previously, Angular developers had to implement this manually. Previously, anyone who wanted to implement this idea in Angular had to do it manually. Angular 17 drastically simplifies this task with the new @defer block (Listing 6).
Listing 6
@defer (on viewport) { <app-recommentations [productGroup]="product().productGroup"> </app-recommentations> } @placeholder { <app-ghost-products></app-ghost-products> }
The use of @defer delays the loading of the specified component (specifically the loading of the specified page area) until a certain event occurs. As a replacement, it presents the placeholder specified under @placeholder. In the demo application used here, ghost elements for the product suggestions are initially presented in this way (Figure 2).
Figure 2: Ghost Elements as placeholder
After loading, @defer swaps the ghost elements for the actual suggestions (Figure 3).
Figure 3: @defer exchanges the placeholder for the delayed loaded component
In this example, the on viewport event is used. It occurs as soon as the placeholder has been scrolled into the visible area of the browser window. Other supported events can be found in Table 1.
Trigger | Description |
on idle | The browser reports that no critical tasks are currently pending (default). |
on viewport | The placeholder is loaded into the visible area of the page. |
on interaction | The user begins to interact with the placeholder. |
on hover | The mouse cursor is moved over the placeholder. |
on immediate | As soon as possible after loading the page. |
on timer ( < duration >) | After a certain time, e.g. on timer(5s) to trigger loading after 5 seconds. |
when < condition > | As soon as the specified condition is met, e.g. when (userName !=== null) |
Table 1: Trigger for @defer
The triggers on viewport, on interaction and on hover force the specification of a @placeholder block by default. Alternatively, they can also refer to other parts of the page that are to be referenced via a template variable:
<h1 #recommentations>Recommentations</h1> @defer (on viewport(recommentations)) { <app-recommentations […] />}
In addition, @defer can be instructed to preload the bundle at an earlier time. As with the preloading of routes, this procedure ensures that the bundles are available as soon as they are needed:
@defer(on viewport; prefetch on immediate) { […] }
In addition to @placeholder, @defer also offers two other blocks: @loading and @error. Angular displays the former while it’s loading the bundle and the latter in the event of an error. To avoid flickering, @placeholder and @loading can be configured with a minimum display duration. The minimum property defines the desired value:
@defer ( […] ) { […] } @loading (after 150ms; minimum 150ms) { […] } @placeholder (minimum 150ms) { […] }
The after property also specifies that the loading indicator should only be displayed if loading takes longer than 150 ms.
Build performance with esbuild
Originally, the Angular CLI used webpack to build bundles. However, webpack is a bit outdated and is currently being challenged by newer tools that are easier to use and much faster. One of these tools is esbuild [esbuild], which has a notable adoption rate of over 20,000 downloads per week.
The CLI team has been working on an esbuild integration for several releases. In Angular 16, this integration was already included in the developer preview stage. As of Angular 17, this implementation is stable and is used as standard for new Angular projects via the Application Builder described below.
EVERYTHING AROUND ANGULAR
The iJS Angular track
For existing projects, it’s worth considering switching to esbuild. To do this, update the builder entry in angular.json:
"builder": "@angular-devkit/build-angular:browser-esbuild"
In other words, add -esbuild at the end. In most cases, ng serve and ng build should then behave as usual, but much faster. The former uses the vite dev-server [vite] for acceleration to build npm packages only when required. The CLI team has also planned further performance optimizations.
The call of ng build could also be drastically accelerated using esbuild. A factor of 2 to 4 is often quoted as the bandwidth.
Easily enable SSR with the new Application Builder
Angular 17 has also drastically simplified support for server-side rendering (SSR). When generating a new project with ng new, a –ssr switch is now available. If this is not used, the CLI asks whether it should set up SSR (Figure 4).
Figure 4: ng new sets up SSR on request
To activate SSR later, simply add the @angular/ssr package:
ng add @angular/ssr
As the scope @angular makes clear, this package comes directly from the Angular team, serving as the successor to the Angular Universal community project. The CLI team has added a new builder that integrates SSR into ng build and ng serve. This application builder uses the above-mentioned esbuild integration to create bundles that can be used both in the browser and on the server side.
A call to ng serve starts a development server that both renders on the server side and delivers the bundles for operation in the browser. A call to ng build –ssr creates bundles for both the browser and server, as well as building a simple Node.js-based server whose source code generates the above-mentioned schematics.
If you cannot or don’t want to run a Node.js server, you can use ng build –prerender to prerender the individual routes of the application during the build.
Further innovations
In addition to the innovations discussed so far, Angular 17 brings numerous other enhancements:
- The router now supports the View Transitions API. This API offered by some browsers allows the animation of transitions using CSS animations, e.g. from one route to another. This optional feature must be activated when setting up the router using the withViewTransitions function. For demonstration purposes, the enclosed example uses CSS animations taken from View Transitions API.
- Signals, which were introduced in version 16 as a developer preview, are now stable. One important change is that they are now designed to be used with immutable data structures by default. This makes it easier for Angular to track changes to data managed by signals. The set method, which assigns a new value, or the update method, which maps the existing value to a new one, can be used to update Signals. The mutate method has been removed, because it doesn’t match the semantics of immutables.
- Now there’s a diagnostic that issues a warning if the getter is not called when reading signals in templates (e.g. {{ products }} instead of {{ products() }}).
- Animations can now be loaded lazy Lazy Animations.
- The Angular CLI generates standalone components, directives and pipes by default. By default, ng new also provides for the bootstrapping of a standalone component. This behavior can be deactivated with the –standalone false switch.
- The ng g interceptor instruction generates functional interceptors.
Summary
Angular’s renaissance continues with version 17, which introduces several new features and improvements. One of the most notable changes is the new control flow, which simplifies the structure of templates. Thanks to deferred loading, less important page areas can be reloaded at a later point in time, speeding up the initial page load. Other features include the use of esbuild, causing the ng build and ng serve instructions run noticeably faster. In addition, the CLI now directly supports SSR and prerendering.